Abstraction @ The Code-First Phase: Architecture 101 (Responsibilities, Concerns, Architectural Styles, Patterns & MVC)

Last updated: Feb 24th, 2024

Major Topics: The Metaphysics, Abstraction, Architectural Styles, Architectural Patterns

Topics: Responsibilities, Concerns, Abstraction Layers, MVC

Whenever you have to achieve a goal or solve anything really, you have to break it down into smaller, more manageable parts.

Remember the TV remote example from The Metaphysics?

Clicking the button “ON” is way easier than needing to understand the language of 0’s and 1’s and receivers and transcoding.

Or consider this…

…How often do you think about “all this stuff (shown below)” when you’re adding a new friend on Instagram?

Or how about all of “this stuff”?

Probably not very often.

And that’s the beauty of abstraction.

Problem. Decomposition.


💡 What we'll cover here: Abstraction is one of the two main metaphysical mental models that all developers need to master. At the Code-First phase, we’ll improve our abstraction skills by shining a light on how we tend to unconsciously use abstraction to solve problems. We’ll do this by learning about responsibilities, concerns, abstraction layers & how they relate to the importantly major topic of architectural styles & patterns.

Lesson goals

Specifically, this is what we’ll do:

  • First, a quick reminder of abstraction & how you’ll use it at the Code-First phase
  • An introduction to the main elements of abstraction: responsibilities, concerns & abstraction layers
  • Learn about architectural patterns, styles & how they relate to abstraction

Metaphysical Essential in Focus: Abstraction

What is abstraction again?

This should be making some sense to you now.

Anytime you have to solve a problem or implement some functionality, lots must be done and known.

Those are important words.

From sending a request, to validating a form, securing the server, caching requests, calling external services, saving to databases, so on, and so forth…

Abstraction lets us break that problem up into smaller and smaller, more complex, more focused, more cohesive parts. Parts that are focused on a specific aspect of solving the problem at a time.

This makes code generally much easier to maintain, understand, discover where to change things, and so on.

How we’ll refine abstraction @ the Code-First phase

At the Code-First phase, the way you’ll use abstraction is simple.

Build a fullstack application while PAYING ATTENTION to the various layers, concerns & responsibilities.

Let’s define those terms now.

Understanding Responsibilities, Concerns & Abstraction Layers

A responsibility is something that is done or known

I never really liked overly simplistic examples, but this is actually a good one.

Check this out.

function add (a: number, b: number) { 
  return a + b; 
}

Would you say this function does things or knows things?

Well, it’s kinda both.

It mostly does adding.

function add (a: number, b: number) { // Responsibility: doing the adding 
  return a + b; 
}

OK, now another one.

Let’s consider the frontend — a React component. Take a feature where you edit a user profile.

import React, { useState } from 'react';
import axios from 'axios';

const UserProfile = () => {
  const [user, setUser] = useState({
    name: '',
    email: '',
    bio: ''
  });

  const handleChange = (event) => {
    setUser({
      ...user,
      [event.target.name]: event.target.value
    });
  };

  const handleSubmit = (event) => {
    event.preventDefault();

    axios
      .put('/api/profile', user)
      .then(response => {
        console.log('Profile updated successfully');
        // Handle success
      })
      .catch(error => {
        console.error('Error updating profile:', error);
        // Handle error
      });
  };

  return (
    <div>
      <h2>Edit Profile</h2>
      <form onSubmit={handleSubmit}>
        <label>
          Name:
          <input
            type="text"
            name="name"
            value={user.name}
            onChange={handleChange}
          />
        </label>
        <br />
        <label>
          Email:
          <input
            type="email"
            name="email"
            value={user.email}
            onChange={handleChange}
          />
        </label>
        <br />
        <label>
          Bio:
          <textarea
            name="bio"
            value={user.bio}
            onChange={handleChange}
          />
        </label>
        <br />
        <button type="submit">Save</button>
      </form>
    </div>
  );
};

export default UserProfile;

What are all the responsibilities involved?

Among the many responsibilities, this code implements are the following:

  • Doings
    • Handling what happens after a button click
    • Saving the current state of the form
    • Rendering the form
  • Knowings
    • Knowing where to send the request
    • Knowing how to structure the request

You could go through each of these lines of code and basically identify all of these as responsibilities.

✨Disclaimer: This is a Responsibility-First way to think about the way you decompose problems and build abstractions. So, it might not be that intuitive to you at the moment. It’s a skill & we’ll build on it throughout the phases. I’m showing this to you now because I want us to start with the end in mind, so you know where we’re going

Now, what’s a concern?

A concern is a category or grouping of responsibilities

Concerns are categories of responsibilities.

Therefore, we might be able to categorize all these responsibilities into the following sets:

  • state management logic
  • ui logic
  • networking/api logic

There’s a lot more than just that involved, but this should suffice to explain what concerns are: categories of logic.

And every operation (see Vertical Slicing) we do typically involves a number of different concerns (Horizontal Decoupling).

Abstraction Layers (of concern): It’s advisable to separate (decouple) concerns

The problem here?

Well, in this React Component example, we’ve coupled all our concerns together in one single place.

“What do you mean by coupling, Khalil?”

Coupling is the degree of interdependence between modules, components, or concerns.

And what have we done here?

We have all these different concerns — none of them carved out into their own specific objects, functions or classes.

Instead, we’re seeing a lot of concerns in a single function.

That means it’s tightly coupled.

The stuff that has to do with state management knows about the ui logic stuff, which also knows about the networking & api logic stuff.

And it’s all blended together. 👇🏼

import React, { useState } from "react";
import axios from "axios";

/**
 * Here's just a taste of some of the responsibilities.
 * See if you can find more!
 */

const UserProfile = () => {
  // 🧱 Knowing: the structure of a user (state management)
  // 🧱 Doing: Changing the user state (state management)
  const [user, setUser] = useState({
    name: "",
    email: "",
    bio: "",
  });

  // 🧱 Doing: Applying the event's value to user state (state management)
  const handleChange = (event) => {
    setUser({
      ...user,
      [event.target.name]: event.target.value,
    });
  };

  // 🖼️ Doing: Initiating the "update profile sequence of interactions"
  // (ui logic)
  const handleSubmit = (event) => {
    event.preventDefault();

    axios
      // 🌐 Knowing: Where the API endpoint is (networking logic)
      // 🌐 Knowing: How to structure the request data (networking logic)
      .put("/api/profile", user)
      .then((response) => {
        // 🖼️ Knowing: What to do after the API call succeeds (ui logic)
        console.log("Profile updated successfully");
        // Handle success
      })
      .catch((error) => {
        // 🖼️ Knowing: What to do after the API call fails (ui logic)
        console.error("Error updating profile:", error);
        // Handle error
      });
  };

  return (
    // 🎨 Knowing: The structure of the layout (presentational logic)
    <div>
      <h2>Edit Profile</h2>

      {/** 🎨 Knowing: The elements of the form (presentational logic) **/}
      <form onSubmit={handleSubmit}>
        <label>
          Name:
          <input
            type="text"
            name="name"
            value={user.name}
            onChange={handleChange}
          />
        </label>
        <br />
        <label>
          Email:
          <input
            type="email"
            name="email"
            value={user.email}
            onChange={handleChange}
          />
        </label>
        <br />
        <label>
          Bio:
          <textarea name="bio" value={user.bio} onChange={handleChange} />
        </label>
        <br />
        <button type="submit">Save</button>
      </form>
    </div>
  );
};

export default UserProfile;

There's a lot of reasons why this is troublesome — the main one being that it kills our ability to test effectively — but at the code-first level, unless you're first aware of concerns, you're not going to be able to separate (decouple) them.

Introducing Architectural Styles & Patterns

Architectural styles

Because there are many ways to solve problems, there are many ways to organize responsibilities & concerns.

If you'll recall, idea of architecture, one of the 4 pillars is to impose a generalized structure to how we organize our codebase & our concerns.

Architectural styles are theoretical suggestions for how to organize our concerns. There are 3 main styles: structural, message-based or distributed.

For example, if we were to take on a message-based style, it'd mean that we'd organize a lot of our code around the idea of messages, events and asynchronicity.

If we were to take a distributed approach, we might break our codebase up into several autonomous parts, each collaborating with each other. This can be as simple as a frontend talking to a backend (client-sever) or as complex as microservices.

Architectural patterns

If architectural styles are the theory, then Architectural patterns what we actually put into practice.

These are the actual physical implementations that everyone knows about. They give us concrete rules and guidelines to follow in order to do them correctly.

The MVC architectural pattern

The most common architectural pattern is the MVC Pattern, which is a sort of layered (structural) style.

It's probably one of the most foundational tools in every web developers' toolbox, looks the most like what we talk about when we're referring to Horizontal Decoupling and makes the most sense to learn first.

It gives us three main concern-buckets to separate our Responsibilities into: model, view, and controller.

Generally speaking:

  • The model handles data and business logic
  • The view handles the presentation
  • The controller acts as the intermediary between the model and the view

Like a hamburger stack of concerns, by separating them from each other, we get a more modular, maintainable codebase — only so long as developers understand that this is what we're doing and continue to place code in the right place, that is.

💡 This is the idea of Horizontal Decoupling, organizing concerns into layers using the layered architectural style. It is extremely common and preferable in most cases.

 

Trivial example: refactoring the React code

Going based on what we’ve just seen, we could potentially refactor the React code like this.

A file for the model.

// UserModel.js
import { useState } from 'react';

const useUserModel = () => {
  const [user, setUser] = useState({
    name: '',
    email: '',
    bio: ''
  });

  const updateUser = (updatedUser) => {
    setUser(updatedUser);
  };

  return { user, updateUser };
};

export default useUserModel;

A file for the view.

// UserView.js
import React from 'react';

const UserView = ({ user, handleChange, handleSubmit }) => {
  return (
    <div>
      <h2>Edit Profile</h2>
      <form onSubmit={handleSubmit}>
        <label>
          Name:
          <input
            type="text"
            name="name"
            value={user.name}
            onChange={handleChange}
          />
        </label>
        <br />
        <label>
          Email:
          <input
            type="email"
            name="email"
            value={user.email}
            onChange={handleChange}
          />
        </label>
        <br />
        <label>
          Bio:
          <textarea
            name="bio"
            value={user.bio}
            onChange={handleChange}
          />
        </label>
        <br />
        <button type="submit">Save</button>
      </form>
    </div>
  );
};

export default UserView;

And a file for the controller.

// UserController.js
import React from 'react';
import axios from 'axios';
import useUserModel from './UserModel';
import UserView from './UserView';

const UserController = () => {
  const { user, updateUser } = useUserModel();

  const handleChange = (event) => {
    const { name, value } = event.target;
    updateUser({ ...user, [name]: value });
  };

  const handleSubmit = (event) => {
    event.preventDefault();

    axios
      .put('/api/profile', user) // Replace '/api/profile' with your actual API endpoint
      .then(response => {
        console.log('Profile updated successfully');
        // Handle success
      })
      .catch(error => {
        console.error('Error updating profile:', error);
        // Handle error
      });
  };

  return <UserView user={user} handleChange={handleChange} handleSubmit={handleSubmit} />;
};

export default UserController;

Linking that all together, we’d be able to set up the feature like this:

import React from 'react';
import UserController from './UserController';

const App = () => {
  return (
    <div>
      <UserController />
    </div>
  );
};

export default App;

There’s still a lot of issues with this, but nonetheless, this is a good first start.

We’ll discuss Horizontal Decoupling more in later lessons.

MVC is generic: frontend, backend, application

One important thing to know is that MVC is generic.

When we're talking about MVC, we could be referring to either:

  • the frontend's MVC
  • the backend's MVC
  • the entire application's MVC (frontend AND backend)

That's because regardless of the system – whether it's keypresses into a form or a network request into a server – we always need:

  • something to interface with the outside world (view)
  • something to represent data and behavior (model)
  • and something to link it all together (controller)

Is that correct?

Not particularly, but this is what got me into the industry and what you need at this level to begin.

What are the most common problem Code-First developers face regarding responsibilities, concerns & organizing them into abstraction layers?

The big problems Code-First developers face.

  • 1) unknown unknowns: It's easy to miss entire concerns due to beginner ignorance. This is just the lack of awareness of the fact that there are more parts to the problem to solve. For example, it's really easy to not know that you need to use CORS or dedicate a layer of code to dealing with validation logic when you're first starting out.
  • 2) incomplete or partially implemented concerns: It's common to implement concerns partially, implementing some, but missing other important responsibilities within that concern. For example, you might validate emails, but forget to validate other fields.
  • 3) mixed concerns/responsibilities (ie: spaghetti code): This is when we mix responsibilities. Basically, you've got a lot of disorganized code in a single class or function.

Summary

  • With respect to Abstraction, we just need to notice that software is just made from responsibilities (doings & knowings). And these responsibilities talk to each other.
  • There are lots of different ways to divide our applications up, and we generally call these Architectural Styles such as Layered architectures and Distributed or Event-Based. They give us a consistent approach to know where to put code, how to organize the abstractions we create, and how they’ll interact.
  • The most common, simplest form is the Layered architectural style, which is where we group responsibilities into layers of concerns (abstraction layers) which talk to each other. The most trivial way to implement this is using the MVC pattern.
  • By having these specific layers of concerns, we make software development easier to communicate, easier to manage, and easier to understand, and we can even allow other teams and people to build parts of the problem for us.
 

Daniel Eduardo Marcano Carrero

Hi Khalil, very valuable lesson here on the MCV architectural pattern. In my mind, it was quite more obscure than you have actually explained it to be. It makes much more sense to me now, and as far as I have understood, it's a good starting point, but it's basically just helpful in the code-first phase of craftship, right?

I have a doubt with the diagram within the section called "The problem? There are more than 3 categories of concerns", as I did not understand the part where it mentions "This is where Code-First devs run into problems. It belongs here...", and then it says "...but they place it here", are you talking about the "Modeling behavior" step?

REPLY
Daneel Malgas

Typo:
- It’s gives us...
should be
- It gives us...

REPLY
Alexandre Rocha

I'm a backend developer that always runs away from tasks that touch the FE 😅, but this refactor in the react component is mindblowing becase I never saw FE developers stop to worry about this level of organization

🤯🤯🤯🤯

REPLY
Khalil Stemmler

The rabbit hole goes deep, my friend.

REPLY